//
// Created by edward on 23-3-16.
//

#include <cmath>
#include "Renderer.h"

namespace {
    constexpr float kStep = 0.01f;
}

/// callback function when window is resized
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
    glViewport(0, 0, width, height);
}



void Renderer::mouseCallback(float xoffset, float yoffset) {
    camera->changeDir(xoffset, yoffset);
}

/// detect keyboard input
void Renderer::processInput(float deltaTime) {
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera->move(deltaTime, Camera::Dir::FORWARD);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera->move(deltaTime, Camera::Dir::BACKWARD);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera->move(deltaTime, Camera::Dir::LEFT);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera->move(deltaTime, Camera::Dir::RIGHT);
    if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS)
        camera->move(deltaTime, Camera::Dir::UP);
    if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS)
        camera->move(deltaTime, Camera::Dir::DOWN);

    static bool isAltPressed = false;  /// Preventing key debouncing

    if (glfwGetKey(window, GLFW_KEY_LEFT_ALT) == GLFW_PRESS) {
        if (!isAltPressed) {
            mouse->flip();
            isAltPressed = true;
        }
    } else {
        isAltPressed = false;
    }


}

Renderer::Renderer(int w, int h)
    : width(w)
    , height(h)
    , fov(M_PI_4)
    {
        init();
}

void Renderer::init() {
    glfwInit();
    /// confirm the version of GLFW is 3.3 or higher
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    /// confirm use the core-profile
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    /// for Mac OS
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    /// create window object, which holds all the windowing data
    window = glfwCreateWindow(width, height, "LearnOpenGL", NULL, NULL);
    if (window == NULL) {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        exit(1);
    }
    /// make the context of window the main context on the current thread
    glfwMakeContextCurrent(window);

    /// initialize GLAD
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cout << "Failed to initialize GLAD" << std::endl;
        exit(1);
    }

    /// set the size of rendering window, perform viewport transformation
    glViewport(0, 0, width, height);

    /// set callback before render loop
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    /// open depth testing
    glEnable(GL_DEPTH_TEST);
    /// set read-only depth buffer
    // glDepthMask(GL_FALSE);
    /// set depth test function -> default is GL_LESS
    // glDepthFunc(GL_LESS);

    /// open stencil testing
    glEnable(GL_STENCIL_TEST);
    glStencilMask(0xFF); // each bit is written to the stencil buffer as is
    // glStencilMask(0x00); // each bit ends up as 0 in the stencil buffer (disabling writes)


    /// set camera
    camera = std::make_unique<Camera>(glm::vec3(0.0f, 0.0f, 3.0f));

    /// set mouse
    mouse = std::make_unique<Mouse>(window);

    /// set user pointer
    glfwSetWindowUserPointer(window, this);

}

void Renderer::loadShader() {
    shader = std::make_unique<Shader>("../shader/vertex.glsl", "../shader/fragment.glsl");
    lightShader = std::make_unique<Shader>("../shader/vertex.glsl", "../shader/lightFragment.glsl");
    setProjection();
}

void Renderer::setProjection(float fovOffset) {
    fov -= fovOffset * 0.1;
    if (fov < 0.1) fov = 0.1;
    if (fov > M_PI_2) fov = M_PI_2;
    auto projection = glm::perspective(fov, static_cast<float>(width) / height, 0.1f, 100.0f);
    shader->use();
    shader->setProjectionMatrix(projection);
    lightShader->use();
    lightShader->setProjectionMatrix(projection);
}

void Renderer::loadModel() {
    model = std::make_unique<Model>("/home/edward/code/computerGraphics/learnOpenGL/model/backpack/backpack.obj");
    light = std::make_unique<Model>("/home/edward/code/computerGraphics/learnOpenGL/model/light/source/Pokeball.fbx");
}


void Renderer::render() {
    loadShader();
    loadModel();
    loop();
}

void Renderer::loop() {
    float lastFrameTime = glfwGetTime();
    float currentFrameTime;
    /// render loop
    while(!glfwWindowShouldClose(window)) {
        ///an iteration of the render loop -> frame

        /// detect keyboard input
        currentFrameTime = glfwGetTime();
        processInput(currentFrameTime - lastFrameTime);
        lastFrameTime = currentFrameTime;

        /// clear color buffer bits with specified color
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

        /// rendering commands
        draw();

        /// handle events
        glfwPollEvents();
        /// swap the color buffer -> double buffer
        glfwSwapBuffers(window);

    }
    glfwTerminate();
}

void Renderer::loadLight(glm::vec3 lightPos) {
    shader->use();

    shader->set("ambientFactor", 0.1f);
    shader->set("diffuseFactor", 0.5f);
    shader->set("specularFactor", 0.7f);
    shader->set("emitFactor", 0.3f);
    shader->set("material.shiness", 0.4f);

    auto lightDir = glm::vec3(-0.2f, -1.0f, -0.3f);
    shader->set("dirLight.color", glm::vec3(1.0f));
    shader->set("dirLight.direction", lightDir);

    constexpr int kMaxPointLight = 1;
    glm::vec3 pointLightPositions[kMaxPointLight] = {
            lightPos
    };
    std::string pointLights = "pointLights[0]";
    const int kIdx = pointLights.size() - 2;
    for (int i = 0; i < kMaxPointLight; ++i) {

        shader->use();
//        std::cout << pointLights << std::endl;
        shader->set(pointLights + ".position", lightPos);
        shader->set(pointLights + ".color", glm::vec3(1.0f));

        /// light distance -> 50
        shader->set(pointLights + ".k0", 1.0f);
        shader->set(pointLights + ".k1", 0.09f);
        shader->set(pointLights + ".k2", 0.032f);

        ++pointLights[kIdx];
    }


    shader->set("spotLight.position", camera->getCameraPos());
    shader->set("spotLight.direction", camera->getCameraDir());
    shader->set("spotLight.color", glm::vec3(1.0f));

    /// light distance -> 20
    shader->set("spotLight.k0", 1.0f);
    shader->set("spotLight.k1", 0.22f);
    shader->set("spotLight.k2", 0.20f);
    shader->set("spotLight.inner", static_cast<float>(cos(glm::radians(12.5f))));
    shader->set("spotLight.outer", static_cast<float>(cos(glm::radians(17.5f))));

}

void Renderer::draw() {
    glm::mat4 mtx(1.0f);
    shader->use();
    shader->setModelMatrix(mtx);
    camera->lookAt(shader.get());

    float t = glfwGetTime();
    auto lightPos = glm::vec3(0.0f, 3.0f * sin(t), -4.0f);

    loadLight(lightPos);

    model->draw(shader.get());

    mtx = glm::translate(mtx, lightPos);
    mtx = glm::scale(mtx, glm::vec3(0.002f));
    mtx = glm::rotate(mtx, glm::radians(100.0f), glm::vec3(1.0f, 0.0f, 0.0f));
    lightShader->use();
    lightShader->setModelMatrix(mtx);
    camera->lookAt(lightShader.get());
//    light->showBorder();
    light->draw(lightShader.get());
}

Renderer::~Renderer() {
    /// optional -> de-allocate all resources

}

